跳到主要内容

SpringCloud Ribbon 负载均衡

参考资料 官方文档地址 参考资料 SpringCloud实战微服务之——Ribbon详解

需要解决的问题

  1. 如何在配置 Eureka Client 注册中心时不去硬编码 Eureka Server 的地址?
  2. 在微服务不同模块间进行通信时,如何不去硬编码服务提供者的地址?
  3. 当部署多个相同微服务时,如何实现请求时的负载均衡? 

实现负载均衡方式1:通过服务器端实现负载均衡(nginx)

实现负载均衡方式2:通过客户端实现负载均衡。

Ribbon 是什么?

Ribbon 是 Netflix 发布的云中间层服务开源项目,其主要功能是提供客户端实现负载均衡算法。Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,Ribbon 是一个客户端负载均衡器,我们可以在配置文件中 Load Balancer 后面的所有机器,Ribbon 会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器,我们也很容易使用 Ribbon 实现自定义的负载均衡算法。

说白了就是用来处理负载均衡,例如订单服务可以使用多个服务器来共同面对高并发量,这时就需要使用负载均衡了,合理的把各个请求分摊到各个服务器上,不至于 “一核工作多核围观的” 的情况

下图展示了Eureka使用Ribbon时的大致架构: 

Ribbon 工作时分为两步:

第一步选择 Eureka Server,它优先选择在同一个 Zone且负载较少的 Server; 第二步再根据用户指定的策略,再从 Server取到的服务注册列表中选择一个地址。其中 Ribbon提供了很多策略,例如轮询round robin、随机Random、根据响应时间加权等。

注意:Ribbon 不是和 Eureka 强绑定的,其它的服务也可以使用

配置环境

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

注意:实际上在 spring-cloud-starter-netflix-eureka-client 里面将包含了这个 ribbon 了,所以如果使用是 eureka 无需再次引入

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

需要需要负载均衡的服务名字要一样

spring:
application:
name: search

然后更改下客户端的代码(加上 @LoadBalanced 注解就能自动去加载负载均衡了)

@Bean
@LoadBalanced // 核心注解,一定要加上
public RestTemplate restTemplate(){
return new RestTemplate();
}

然后调用就无需像上面那么麻烦了,直接在 ip 那里填上服务的名称,restTemplate 就会自动找到这个负载均衡的服务了

@GetMapping("/customer")
public String customer(){
// 直接在 ip栏这里填上上面的那个服务名 search,这里的 /SEARCH 是路由地址(其实大小写都可以,不过使用大小比较容易区分)
String response = restTemplate.getForObject("http://SEARCH/search", String.class);
return "this is customer model use" + response;
}

负载均衡策略

只需随便在客户端找个地方(例如启动类)加上下面这个 IRule 这个 Bean,就能更改负载均衡策略了

注意:启动类里面内置了 @ComponentScan

@Bean
public IRule robbinRule() {
// 随机策略
return new RandomRule();
}

但是如果被 @ComponentScan 扫描到了这个规则,它会默认被当成全局规则,如果想要对某个服务进行定制化配置规则,最好配置到 @ComponentScan 扫描不到的地方去,具体细节看下面负载规则的替换那一节

IRule 接口

这个 IRule 是负载均衡策略的接口,主要是用于定义 LoadBalancer

public interface IRule{
public Server choose(Object key);

public void setLoadBalancer(ILoadBalancer lb);

public ILoadBalancer getLoadBalancer();
}

它的实现类如下所示

默认使用的就是 RoundRobbinRule(轮询策略)

常用策略

1、RandomRule:随机

2、RoundRobbinRule:轮询策略(默认)

3、WeightedResponseTimeRule:开始是轮询策略,后续会根据服务的响应时间,自动分配权重

4、BestAvailableRule:根据被调用方并发数最小的去分配(就是优先分发给被调用次数最少的)

负载规则的替换

配置 Ribbon 最简单的方式就是通过配置文件实现。

# 可以在配置文件为每个服务配置它专属的 Rule

# 指定需要更变策略的服务名称
search: # 服务1
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
provider: # 服务2
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

具体配置方式参考 Spring cloud系列六 Ribbon的功能概述、主要组件和属性文件配置

当然我们也可以通过代码的方式来配置。

@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();
}
}

官方文档明确给出了警告:大体意思是对于 Ribbon 的配置必须用 @Configuration 注解标识,并且不能被 @Component 注解或者 @SpringBootApplication(因为里面包含了 @ComponentScan)扫描到。因为如果被 @ComponetScan 扫描到会导致所有的 RibbonClient 都去共享这个配置,达不到特殊化定制的目的了

解决方案如下:

@EnableEurekaClient
@SpringBootApplication
@ComponentScan(
basePackages = "com.alsritter",
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = MySelfRule.class)}
)
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

创建一个 Ribbon 客户端的配置类,关联 BeanConfiguration,用 name 来指定调用的服务名称,代码如下所示:

@Configuration
// 如果多个服务可以选择不同的策略
/*@RibbonClients({
@RibbonClient(name = "other",configuration = OtherConfig.class),
@RibbonClient(name = "provider",configuration = ProviderConfig.class)
})*/
// 这里表示访问这个 provider-service 服务时采用上面配置的 MySelfRule 策略
@RibbonClient(name = "provider-service", configuration = MySelfRule.class)
public class RibbonConfig {

@Bean
@LoadBalanced // 核心注解
public RestTemplate restTemplate(){
return new RestTemplate();
}

}

这时再次启动会发现变成了随机选择

轮询算法原理

轮询算法原理:

rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标(每次服务重启动后 rest 接口计数从1开始)

举例:

总服务集群数量:3
第 i 次请求

3 % 3 = 0 -> index = 0 list.get(index);
4 % 3 = 1 -> index = 1 list.get(index);
5 % 3 = 2 -> index = 2 list.get(index);
6 % 3 = 0 -> index = 0 list.get(index);

看完原理,再来分享一下源码,老规矩,去掉部分健壮性判断,保留核心代码

public Server choose(ILoadBalancer lb, Object key) {

/* ... */

Server server = null;
int count = 0;

while (server == null && count++ < 10) {
// 取得可达的服务器
List<Server> reachableServers = lb.getReachableServers();
// 取得全部服务器
List<Server> allServers = lb.getAllServers();

int upCount = reachableServers.size();
int serverCount = allServers.size();

int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex); // 取得当前使用哪个服务器

// 为空的话则重新上面的过程
if (server == null) {
/* Transient. */
Thread.yield(); // 这里其实不用管,暂时让出 CPU,实际影响不大
continue;
}

if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}

// 注意,这个 nextServerCyclicCounter 是原子 Integer 表示线程安全的,他就是用来访问计数用的
private AtomicInteger nextServerCyclicCounter;

// 为了线程安全以及效率,这里取模使用的是 CAS(乐观锁)
private int incrementAndGetModulo(int modulo) {
// 这里是一段 CAS 锁
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}